/**
 * $RCSfile: ProfiledConnection.java,v $
 * $Revision: 1.4 $
 * $Date: 2001/08/19 20:13:57 $
 *
 * Copyright (C) 1999-2001 CoolServlets Inc. All rights reserved.
 * ===================================================================
 * The Jive Software License (based on Apache Software License, Version 1.1)
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The end-user documentation included with the redistribution,
 *    if any, must include the following acknowledgment:
 *       "This product includes software developed by
 *        Jive Software (http://www.jivesoftware.com)."
 *    Alternately, this acknowledgment may appear in the software itself,
 *    if and wherever such third-party acknowledgments normally appear.
 *
 * 4. The names "Jive" and "CoolServlets" must not be used to
 *    endorse or promote products derived from this software without
 *    prior written permission. For written permission, please
 *    contact webmaster@coolservlets.com.
 *
 * 5. Products derived from this software may not be called "Jive",
 *    nor may "Jive" appear in their name, without prior written
 *    permission of CoolServlets.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL COOLSERVLETS INC OR
 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 * ====================================================================
 * This software consists of voluntary contributions made by many
 * individuals on behalf of Jive Software. For more information
 * on Jive Software please visit http://www.jivesoftware.com.
 */

package com.jivesoftware.forum.database;

import java.io.InputStream;
import java.io.Reader;
import java.math.BigDecimal;
import java.net.URL;
import java.sql.Array;
import java.sql.Blob;
import java.sql.CallableStatement;
import java.sql.Clob;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ParameterMetaData;
import java.sql.PreparedStatement;
import java.sql.Ref;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.SQLWarning;
import java.sql.Savepoint;
import java.sql.Statement;
import java.sql.Struct;
import java.sql.Time;
import java.sql.Timestamp;
import java.util.Calendar;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Map;
import java.util.Properties;

/**
 * Wraps a Connection object and collects statistics about the database queries
 * that are performed.<p>
 *
 * Statistics of the profiled Connections can be obtained from the static
 * methods of this class. Instances of this class are the actual wrappers that
 * perform profiling.
 */
public class ProfiledConnection implements Connection {

    /**
     * Constant for SELECT database queries.
     */
    public static final int SELECT = 0;

    /**
     * Constant for UPDATE database queries.
     */
    public static final int UPDATE = 1;

    /**
     * Constant for INSERT database queries.
     */
    public static final int INSERT = 2;

    /**
     * Constant for DELETE database queries.
     */
    public static final int DELETE = 3;

    private static long startInsertTime = 0;
    private static long startUpdateTime = 0;
    private static long startSelectTime = 0;
    private static long startDeleteTime = 0;

    private static long endInsertTime = 0;
    private static long endUpdateTime = 0;
    private static long endSelectTime = 0;
    private static long endDeleteTime = 0;

    private static long insertCount = 0;
    private static long updateCount = 0;
    private static long selectCount = 0;
    private static long deleteCount = 0;

    private static long totalInsertTime = 0;
    private static long totalUpdateTime = 0;
    private static long totalSelectTime = 0;
    private static long totalDeleteTime = 0;

    private static Hashtable insertQueries = new Hashtable();
    private static Hashtable updateQueries = new Hashtable();
    private static Hashtable selectQueries = new Hashtable();
    private static Hashtable deleteQueries = new Hashtable();

    /**
     * Start profiling.
     */
    public static void start() {
       long now = System.currentTimeMillis();
        startInsertTime = startUpdateTime = startSelectTime = startDeleteTime = now;
    }

    /**
     * Stop profiling.
     */
    public static void stop() {
        endInsertTime = endUpdateTime = endSelectTime = endDeleteTime = 0;
    }

    /**
     * Returns the total number database queries of a particular type performed.
     * Valid types are ProfiledConnection.SELECT, ProfiledConnection.UPDATE,
     * ProfiledConnection.INSERT, and ProfiledConnection.DELETE.
     *
     * @param type the type of query to get the count for.
     * @return the number queries of type <tt>type</tt> performed.
     */
    public static long getQueryCount(int type) {
        switch (type) {
            case SELECT:
                return selectCount;
            case UPDATE:
                return updateCount;
            case INSERT:
                return insertCount;
            case DELETE:
                return deleteCount;
            default:
                throw new IllegalArgumentException("Invalid type");
        }
    }

    /**
     *
     *
     * @param sql the insert sql string.
     * @param time the length of time the query took in milliseconds
     */
    public static void addQuery(int type, String sql, long time) {
        // Do nothing if we didn't receive a sql statement
        if (sql == null || sql.equals("")) {
            return;
        }

        // remove values from query
        sql = removeQueryValues(sql);

        ProfiledConnectionEntry entry = null;
        switch (type) {
            case SELECT:
                selectCount++;
                totalSelectTime += time;
                entry = (ProfiledConnectionEntry)selectQueries.get(sql);
                if (entry == null) {
                    entry = new ProfiledConnectionEntry(sql);
                    selectQueries.put(sql, entry);
                }
                break;
            case UPDATE:
                updateCount++;
                totalUpdateTime += time;
                entry = (ProfiledConnectionEntry)updateQueries.get(sql);
                if (entry == null) {
                    entry = new ProfiledConnectionEntry(sql);
                    updateQueries.put(sql, entry);
                }
                break;
            case INSERT:
                insertCount++;
                totalInsertTime += time;
                entry = (ProfiledConnectionEntry)insertQueries.get(sql);
                if (entry == null) {
                    entry = new ProfiledConnectionEntry(sql);
                    insertQueries.put(sql, entry);
                }
                break;
            case DELETE:
                deleteCount++;
                totalDeleteTime += time;
                entry = (ProfiledConnectionEntry)deleteQueries.get(sql);
                if (entry == null) {
                    entry = new ProfiledConnectionEntry(sql);
                    deleteQueries.put(sql, entry);
                }
                break;
            default:
                throw new IllegalArgumentException("Invalid type");
        }

        entry.count++;
        entry.totalTime += time;
    }

    /**
     * Returns the average number of queries of a certain type that have been
     * performed per second since profiling started. If profiling has been
     * stopped, that moment in time is used for the calculation. Otherwise,
     * the current moment in time is used.
     *
     * @param type the type of database query to check.
     * @return the average number of queries of a certain typed performed per
     *      second.
     */
    public static double getQueriesPerSecond(int type) {
        long count;
        long start, end;

        switch (type) {
            case SELECT:
                count = selectCount;
                start = startSelectTime;
                end = endSelectTime;
                break;
            case UPDATE:
                count = updateCount;
                start = startUpdateTime;
                end = endUpdateTime;
                break;
            case INSERT:
                count = insertCount;
                start = startInsertTime;
                end = endInsertTime;
                break;
            case DELETE:
                count = deleteCount;
                start = startDeleteTime;
                end = endDeleteTime;
                break;
            default:
                throw new IllegalArgumentException("Invalid type");
        }
        //If the profiling hasn't been stopped yet, we want to give
        //profiling values up to the current time instead.
        if (end == 0) {
            end = System.currentTimeMillis();
        }
        //Compute the number of seconds
        double time = (end - start)/1000.0;
        //Finally, return the average.
        return count / time;
    }

    /**
     * Returns the average amount of time spent executing the specified type
     * of query.
     *
     * @param type the type of query.
     * @return a double representing the average time spent executing the type
     *      of query.
     */
    public static double getAverageQueryTime(int type) {
        long time;
        long count;
        switch (type) {
            case SELECT:
                count = selectCount;
                time = totalSelectTime;
                break;
            case UPDATE:
                count = updateCount;
                time = totalUpdateTime;
                break;
            case INSERT:
                count = insertCount;
                time = totalInsertTime;
                break;
            case DELETE:
                count = deleteCount;
                time = totalDeleteTime;
                break;
            default:
                throw new IllegalArgumentException("Invalid type");
        }

        if (count != 0) {
            return time/(double)count;
        }
        else {
            return 0.0;
        }
    }

    /**
     * Returns the total amount of time in milliseconds spent doing a particular
     * type of query. Note that this isn't necessarily representative of actual real
     * time since db queries often occur in parallel.
     *
     * @param type the type of query to check.
     * @return the number of milliseconds spent executing the specified type of
     *      query.
     */
    public static long getTotalQueryTime(int type) {
         switch (type) {
            case SELECT:
                return totalSelectTime;
            case UPDATE:
                return totalUpdateTime;
            case INSERT:
                return totalInsertTime;
            case DELETE:
                return totalDeleteTime;
            default:
                throw new IllegalArgumentException("Invalid type");
        }
    }

    /**
     * Returns an array of sorted queries (as ProfiledConnectionEntry objects) by type
     *
     * @param type the type of query to check
     * @param sortByTime sort the resulting list by Time if true,
     *                   otherwise sort by count if false (default)
     * @return an array of ProfiledConnectionEntry objects
     */
    public static ProfiledConnectionEntry[] getSortedQueries(int type, boolean sortByTime) {
        Hashtable queries;
        switch (type) {
            case SELECT:
                queries = selectQueries;
                break;
            case UPDATE:
                queries = updateQueries;
                break;
            case INSERT:
                queries = insertQueries;
                break;
            case DELETE:
                queries = deleteQueries;
                break;
            default:
                throw new IllegalArgumentException("Invalid type");
        }

        ProfiledConnectionEntry[] result = new ProfiledConnectionEntry[queries.size()];

        // no queries, return null set
        if (queries.size() < 1) {
            return null;
        }

        // since the values of the hashtable contain everything that
        // we need (including the sql statement), ignore the keys
        Enumeration e = queries.elements();

        int c = 0;
        while (e.hasMoreElements()) {
            result[c++] = (ProfiledConnectionEntry) e.nextElement();
        }

        quickSort(result, sortByTime, 0, result.length -1);
        return result;
    }

    /**
     * Reset all statistics.
     */
    public static void resetStatistics() {
        startInsertTime = startUpdateTime = startSelectTime = startDeleteTime = 0;
        endInsertTime = endUpdateTime = endSelectTime = endDeleteTime = 0;
        insertCount = updateCount = selectCount = deleteCount = 0;
        totalInsertTime = totalUpdateTime = totalSelectTime = totalDeleteTime = 0;

        insertQueries.clear();
        updateQueries.clear();
        selectQueries.clear();
        deleteQueries.clear();
    }

   /**
     * @param ProfiledConnectionEntry[] entries
     * @param sortByTime - sort by time if true, otherwise sort by count
     * @param int first - first index to sort on. Normally 0
     * @param int last - last index to sort on. Normally length -1
     */
    public static void quickSort(ProfiledConnectionEntry[] entries, boolean sortByTime, int first, int last) {

        // do nothing if array contains fewer than two elements
        if (first >= last || entries.length < 2) {
            return;
        }

        swap(entries, first, (first+last)/2);

        int index = first;
        for (int i=first+1; i <= last; i++) {
            if (sortByTime && (entries[first].totalTime/entries[first].count) < (entries[i].totalTime/entries[i].count)) {
                swap(entries, ++index, i);
            }
            else if (entries[first].count < entries[i].count) {
                swap(entries, ++index, i);
            }
        }
        swap(entries, first, index);
        quickSort(entries, sortByTime, first, index-1);
        quickSort(entries, sortByTime, index+1, last);
    }

    private static void swap(Object[] list, int i, int j) {
        Object tmp = list[i];
        list[i] = list[j];
        list[j] = tmp;
    }

    private static String removeQueryValues(String _sql) {
        int length = _sql.length();

        if (_sql.indexOf("=") == -1) {
           return _sql;
        }

        StringBuffer sql    = new StringBuffer(_sql);
        boolean inValue     = false;
        boolean afterEquals = false;
        boolean hasQuotes   = false;
        int startValue  = -1;
        int endValue    = -1;
        int charRemoved = 0;

        for (int x = 0; x < length; x++) {
            char c = _sql.charAt(x);

            switch (c) {
                case '=': {
                    if (!afterEquals) {
                        afterEquals = true;
                    }
                    break;
                }
                case ' ': {
                    if (!hasQuotes && inValue) {
                        endValue = x;
                        inValue = false;
                        hasQuotes = false;
                        afterEquals = false;
                    }
                    break;
                }
                case '\'': {
                    if (afterEquals && !inValue) {
                        startValue = x;
                        inValue = true;
                        hasQuotes = true;
                    }
                    else if (afterEquals && inValue && hasQuotes) {
                        endValue = x+1;
                        inValue = false;
                        hasQuotes = false;
                        afterEquals = false;
                    }
                    break;
                }
                case '-': {
                    if (afterEquals && !inValue) {
                        startValue = x;
                        inValue = true;

                    }
                    break;
                }
                case '+': {
                    if (afterEquals && !inValue) {
                        startValue = x;
                        inValue = true;
                    }
                    break;
                }
                case '0': {
                    if (afterEquals && !inValue) {
                        startValue = x;
                        inValue = true;
                    }
                    break;
                }
                case '1': {
                    if (afterEquals && !inValue) {
                        startValue = x;
                        inValue = true;
                    }
                    break;
                }
                case '2': {
                    if (afterEquals && !inValue) {
                        startValue = x;
                        inValue = true;
                    }
                    break;
                }
                case '3': {
                    if (afterEquals && !inValue) {
                        startValue = x;
                        inValue = true;
                    }
                    break;
                }
                case '4': {
                    if (afterEquals && !inValue) {
                        startValue = x;
                        inValue = true;
                    }
                    break;
                }
                case '5': {
                    if (afterEquals && !inValue) {
                        startValue = x;
                        inValue = true;
                    }
                    break;
                }
                case '6': {
                    if (afterEquals && !inValue) {
                        startValue = x;
                        inValue = true;
                    }
                    break;
                }
                case '7': {
                    if (afterEquals && !inValue) {
                        startValue = x;
                        inValue = true;
                    }
                    break;
                }
                case '8': {
                    if (afterEquals && !inValue) {
                        startValue = x;
                        inValue = true;
                    }
                    break;
                }
                case '9': {
                    if (afterEquals && !inValue) {
                        startValue = x;
                        inValue = true;
                    }
                    break;
                }
                default: {
                    if (afterEquals && !inValue) {
                        afterEquals = false;
                    }
                }
            }

            if (x == length-1 && afterEquals) {
                endValue = x+1;
            }

            if (startValue != -1 && endValue != -1) {
                sql.replace(startValue-charRemoved, endValue-charRemoved, "?");

                charRemoved += endValue - startValue - 1;
                startValue = -1;
                endValue = -1;
            }
        }

        return sql.toString();
    }

    //--------------------- Connection Wrapping Code ---------------------//


    private Connection connection;

    /**
     * Creates a new ProfiledConnection that wraps the specified connection.
     *
     * @param connection the Connection to wrap and collect stats for.
     */
    public ProfiledConnection(Connection connection) throws SQLException {
        if (connection != null) {
            this.connection = connection;
        }
        else {
            throw new SQLException("ProfiledConnection constructor was given a " +
                    "null connection");
        }

    }

    public void close() throws SQLException {
        //Close underlying connection.
        if (connection != null) {
            connection.close();
        }
    }

    public String toString() {
        if (connection != null) {
            return connection.toString();
        }
        else {
            return "ProfiledConnection connection wrapper";
        }
    }

    public Statement createStatement() throws SQLException {
        //Returned a TimedStatement so that we can do db timings.
        return new TimedStatement(connection.createStatement());
    }

    public PreparedStatement prepareStatement(String sql) throws SQLException {
        //Returned a TimedPreparedStatement so that we can do db timings.
        return new TimedPreparedStatement(connection.prepareStatement(sql), sql);
    }

    public CallableStatement prepareCall(String sql) throws SQLException {
        return connection.prepareCall(sql);
    }

    public String nativeSQL(String sql) throws SQLException {
        return connection.nativeSQL(sql);
    }

    public void setAutoCommit(boolean autoCommit) throws SQLException {
        connection.setAutoCommit(autoCommit);
    }

    public boolean getAutoCommit() throws SQLException {
        return connection.getAutoCommit();
    }

    public void commit() throws SQLException {
        connection.commit();
    }

    public void rollback() throws SQLException {
        connection.rollback();
    }

    public boolean isClosed() throws SQLException {
        return connection.isClosed();
    }

    public DatabaseMetaData getMetaData() throws SQLException {
        return connection.getMetaData();
    }

    public void setReadOnly(boolean readOnly) throws SQLException {
        connection.setReadOnly(readOnly);
    }

    public boolean isReadOnly() throws SQLException {
        return connection.isReadOnly();
    }

    public void setCatalog(String catalog) throws SQLException {
        connection.setCatalog(catalog);
    }

    public String getCatalog() throws SQLException {
        return connection.getCatalog();
    }

    public void setTransactionIsolation(int level) throws SQLException {
        connection.setTransactionIsolation(level);
    }

    public int getTransactionIsolation() throws SQLException {
        return connection.getTransactionIsolation();
    }

    public SQLWarning getWarnings() throws SQLException {
        return connection.getWarnings();
    }

    public void clearWarnings() throws SQLException {
        connection.clearWarnings();
    }

    public Statement createStatement(int resultSetType, int resultSetConcurrency)
            throws SQLException
    {
        return new TimedStatement(connection.createStatement(resultSetType,
                resultSetConcurrency));
    }

    public PreparedStatement prepareStatement(String sql, int resultSetType,
            int resultSetConcurrency) throws SQLException
    {
        return new TimedPreparedStatement(connection.prepareStatement(
                sql,resultSetType, resultSetConcurrency), sql);
    }

    public CallableStatement prepareCall(String sql, int resultSetType,
            int resultSetConcurrency) throws SQLException
    {
        return connection.prepareCall(sql, resultSetType, resultSetConcurrency);
    }

    public Map getTypeMap() throws SQLException {
        return connection.getTypeMap();
    }

    public void setTypeMap(Map map) throws SQLException {
        connection.setTypeMap(map);
    }

    /**
     * An implementation of the Statement interface that wraps an underlying
     * Statement object and performs timings of the database queries. The class
     * does not handle batch queries but should generally work otherwise.
     */
    class TimedStatement implements Statement {

        private Statement stmt;
        private int type;

        /**
         * Creates a new TimedStatement that wraps <tt>stmt</tt>.
         */
        public TimedStatement(Statement stmt) {
            this.stmt = stmt;
        }

        public void addBatch(String sql) throws SQLException {
            stmt.addBatch(sql);
        }

        public void cancel() throws SQLException {
            stmt.cancel();
        }

        public void clearBatch() throws SQLException {
            stmt.clearBatch();
        }

        public void clearWarnings() throws SQLException {
            stmt.clearWarnings();
        }

        public void close() throws SQLException {
            stmt.close();
        }

        public boolean execute(String sql) throws SQLException {

            long t1 = System.currentTimeMillis();
            boolean result = stmt.execute(sql);
            long t2 = System.currentTimeMillis();

            // determine the type of query
            String sqlL = sql.toLowerCase().trim();

            if (sqlL.startsWith("insert")) {
                addQuery(INSERT, sql, t2-t1);
            }
            else if (sqlL.startsWith("update")) {
                addQuery(UPDATE, sql, t2-t1);
            }
            else if (sqlL.startsWith("delete")) {
                addQuery(DELETE, sql, t2-t1);
            }
            else {
                addQuery(SELECT, sql, t2-t1);
            }
            return result;
        }

        public int[] executeBatch() throws SQLException {
            return stmt.executeBatch();
        }

        public ResultSet executeQuery(String sql) throws SQLException {
            long t1 = System.currentTimeMillis();
            ResultSet result = stmt.executeQuery(sql);
            long t2 = System.currentTimeMillis();

            // determine the type of query
            String sqlL = sql.toLowerCase().trim();

            if (sqlL.startsWith("insert")) {
                addQuery(INSERT, sql, t2-t1);
            }
            else if (sqlL.startsWith("update")) {
                addQuery(UPDATE, sql, t2-t1);
            }
            else if (sqlL.startsWith("delete")) {
                addQuery(DELETE, sql, t2-t1);
            }
            else {
                addQuery(SELECT, sql, t2-t1);
            }
            return result;
        }

        public int executeUpdate(String sql) throws SQLException {
            long t1 = System.currentTimeMillis();
            int result = stmt.executeUpdate(sql);
            long t2 = System.currentTimeMillis();

            // determine the type of query
            String sqlL = sql.toLowerCase().trim();

            if (sqlL.startsWith("insert")) {
                addQuery(INSERT, sql, t2-t1);
            }
            else if (sqlL.startsWith("update")) {
                addQuery(UPDATE, sql, t2-t1);
            }
            else if (sqlL.startsWith("delete")) {
                addQuery(DELETE, sql, t2-t1);
            }
            else {
                addQuery(SELECT, sql, t2-t1);
            }
            return result;
        }

        public Connection getConnection() throws SQLException {
            return stmt.getConnection();
        }

        public int getFetchDirection() throws SQLException {
            return stmt.getFetchDirection();
        }

        public int getFetchSize()  throws SQLException {
            return stmt.getFetchSize();
        }

        public int getMaxFieldSize() throws SQLException {
            return stmt.getMaxFieldSize();
        }

        public int getMaxRows() throws SQLException {
            return stmt.getMaxRows();
        }

        public boolean getMoreResults() throws SQLException {
            return stmt.getMoreResults();
        }

        public int getQueryTimeout() throws SQLException {
            return stmt.getQueryTimeout();
        }

        public ResultSet getResultSet() throws SQLException {
            return stmt.getResultSet();
        }

        public int getResultSetConcurrency() throws SQLException {
            return stmt.getResultSetConcurrency();
        }

        public int getResultSetType() throws SQLException {
            return stmt.getResultSetType();
        }

        public int getUpdateCount() throws SQLException {
            return stmt.getUpdateCount();
        }

        public SQLWarning getWarnings() throws SQLException {
            return stmt.getWarnings();
        }

        public void setCursorName(String name) throws SQLException {
            stmt.setCursorName(name);
        }

        public void setEscapeProcessing(boolean enable) throws SQLException {
            stmt.setEscapeProcessing(enable);
        }

        public void setFetchDirection(int direction) throws SQLException {
            stmt.setFetchDirection(direction);
        }

        public void setFetchSize(int rows) throws SQLException {
            stmt.setFetchSize(rows);
        }

        public void setMaxFieldSize(int max) throws SQLException {
            stmt.setMaxFieldSize(max);
        }

        public void setMaxRows(int max) throws SQLException {
            stmt.setMaxRows(max);
        }

        public void setQueryTimeout(int seconds) throws SQLException {
            stmt.setQueryTimeout(seconds);
        }

		public boolean execute(String sql, int autoGeneratedKeys)
				throws SQLException {
			throw new UnsupportedOperationException(); 
		}

		public boolean execute(String sql, int[] columnIndexes)
				throws SQLException {
			throw new UnsupportedOperationException(); 
		}

		public boolean execute(String sql, String[] columnNames)
				throws SQLException {
			throw new UnsupportedOperationException(); 
		}

		public int executeUpdate(String sql, int autoGeneratedKeys)
				throws SQLException {
			throw new UnsupportedOperationException(); 
		}

		public int executeUpdate(String sql, int[] columnIndexes)
				throws SQLException {
			throw new UnsupportedOperationException(); 
		}

		public int executeUpdate(String sql, String[] columnNames)
				throws SQLException {
			throw new UnsupportedOperationException(); 
		}

		public ResultSet getGeneratedKeys() throws SQLException {
			throw new UnsupportedOperationException(); 
		}

		public boolean getMoreResults(int current) throws SQLException {
			throw new UnsupportedOperationException(); 
		}

		public int getResultSetHoldability() throws SQLException {
			throw new UnsupportedOperationException(); 
		}

		public boolean isClosed() throws SQLException {
			throw new UnsupportedOperationException(); 
		}

		public boolean isPoolable() throws SQLException {
			throw new UnsupportedOperationException(); 
		}

		public void setPoolable(boolean poolable) throws SQLException {
			throw new UnsupportedOperationException(); 
		}

		public boolean isWrapperFor(Class<?> iface) throws SQLException {
			throw new UnsupportedOperationException(); 
		}

		public <T> T unwrap(Class<T> iface) throws SQLException {
			throw new UnsupportedOperationException(); 
		}
    }

    /**
     * An implementation of the PreparedStatement interface that wraps an
     * underlying PreparedStatement object and performs timings of the database
     * queries.
     */
    class TimedPreparedStatement implements PreparedStatement {

        private String sql;
        private PreparedStatement pstmt;
        private int type = SELECT;

        public TimedPreparedStatement(PreparedStatement _pstmt, String _sql) {
            this.pstmt  = _pstmt;
            this.sql    = _sql;

            // determine the type of query
            String sqlL = _sql.toLowerCase().trim();

            if (sqlL.startsWith("insert")) {
                type = INSERT;
            }
            else if (sqlL.startsWith("update")) {
                type = UPDATE;
            }
            else if (sqlL.startsWith("delete")) {
                type = DELETE;
            }
            else {
                type = SELECT;
            }
        }

        public void addBatch() throws SQLException{
            pstmt.addBatch();
        }

        public void clearParameters() throws SQLException {
            pstmt.clearParameters();
        }

        public boolean execute() throws SQLException {
            //Perform timing of this method.
            long t1 = System.currentTimeMillis();
            boolean result = pstmt.execute();
            long t2 = System.currentTimeMillis();

            switch (type) {
                case SELECT:
                    addQuery(SELECT, sql, t2-t1);
                    break;
                case UPDATE:
                    addQuery(UPDATE, sql, t2-t1);
                    break;
                case INSERT:
                    addQuery(INSERT, sql, t2-t1);
                    break;
                case DELETE:
                    addQuery(DELETE, sql, t2-t1);
                    break;
            }
            return result;
        }

        /*
         * This is one of the methods that we wish to time
         */
        public ResultSet executeQuery() throws SQLException {

            long t1 = System.currentTimeMillis();
            ResultSet result = pstmt.executeQuery();
            long t2 = System.currentTimeMillis();

            switch (type) {
                case SELECT:
                    addQuery(SELECT, sql, t2-t1);
                    break;
                case UPDATE:
                    addQuery(UPDATE, sql, t2-t1);
                    break;
                case INSERT:
                    addQuery(INSERT, sql, t2-t1);
                    break;
                case DELETE:
                    addQuery(DELETE, sql, t2-t1);
                    break;
            }
            return result;
        }

        /*
         * This is one of the methods that we wish to time
         */
        public int executeUpdate() throws SQLException {

            long t1 = System.currentTimeMillis();
            int result = pstmt.executeUpdate();
            long t2 = System.currentTimeMillis();

            switch (type) {
                case SELECT:
                    addQuery(SELECT, sql, t2-t1);
                    break;
                case UPDATE:
                    addQuery(UPDATE, sql, t2-t1);
                    break;
                case INSERT:
                    addQuery(INSERT, sql, t2-t1);
                    break;
                case DELETE:
                    addQuery(DELETE, sql, t2-t1);
                    break;
            }
            return result;
        }

        public ResultSetMetaData getMetaData() throws SQLException {
            return pstmt.getMetaData();
        }

        public void setArray(int i, Array x) throws SQLException {
            pstmt.setArray(i, x);
        }

        public void setAsciiStream(int parameterIndex, InputStream x, int length)
                throws SQLException
        {
            pstmt.setAsciiStream(parameterIndex, x, length);
        }

        public void setBigDecimal(int parameterIndex, BigDecimal x)
                throws SQLException
        {
            pstmt.setBigDecimal(parameterIndex, x);
        }

        public void setBinaryStream(int parameterIndex, InputStream x, int length)
                throws SQLException
        {
            pstmt.setBinaryStream(parameterIndex, x, length);
        }

        public void setBlob(int i, Blob x) throws SQLException {
            pstmt.setBlob(i, x);
        }

        public void setBoolean(int parameterIndex, boolean x) throws SQLException {
            pstmt.setBoolean(parameterIndex, x);
        }

        public void setByte(int parameterIndex, byte x) throws SQLException {
            pstmt.setByte(parameterIndex, x);
        }

        public void setBytes(int parameterIndex, byte[] x) throws SQLException {
            pstmt.setBytes(parameterIndex, x);
        }

        public void setCharacterStream(int parameterIndex, Reader reader,
                int length) throws SQLException
        {
            pstmt.setCharacterStream(parameterIndex, reader, length);
        }

        public void setClob(int i, Clob x) throws SQLException {
            pstmt.setClob(i, x);
        }

        public void setDate(int parameterIndex, java.sql.Date x) throws SQLException {
            pstmt.setDate(parameterIndex, x);
        }

        public void setDate(int parameterIndex, java.sql.Date x, Calendar cal)
                throws SQLException
        {
            pstmt.setDate(parameterIndex, x, cal);
        }

        public void setDouble(int parameterIndex, double x) throws SQLException {
            pstmt.setDouble(parameterIndex, x);
        }

        public void setFloat(int parameterIndex, float x) throws SQLException {
            pstmt.setFloat(parameterIndex, x);
        }

        public void setInt(int parameterIndex, int x) throws SQLException {
            pstmt.setInt(parameterIndex, x);
        }

        public void setLong(int parameterIndex, long x) throws SQLException {
            pstmt.setLong(parameterIndex, x);
        }

        public void setNull(int parameterIndex, int sqlType) throws SQLException {
            pstmt.setNull(parameterIndex, sqlType);
        }

        public void setNull(int paramIndex, int sqlType, String typeName)
                throws SQLException
        {
            pstmt.setNull(paramIndex, sqlType, typeName);
        }

        public void setObject(int parameterIndex, Object x) throws SQLException {
            pstmt.setObject(parameterIndex, x);
        }

        public void setObject(int parameterIndex, Object x, int targetSqlType)
                throws SQLException
        {
            pstmt.setObject(parameterIndex, x, targetSqlType);
        }

        public void setObject(int parameterIndex, Object x, int targetSqlType,
                int scale) throws SQLException
        {
            pstmt.setObject(parameterIndex, x, targetSqlType, scale);
        }

        public void setRef(int i, Ref x) throws SQLException {
            pstmt.setRef(i, x);
        }

        public void setShort(int parameterIndex, short x) throws SQLException {
            pstmt.setShort(parameterIndex, x);
        }

        public void setString(int parameterIndex, String x) throws SQLException {
            pstmt.setString(parameterIndex, x);
        }

        public void setTime(int parameterIndex, Time x) throws SQLException {
            pstmt.setTime(parameterIndex, x);
        }

        public void setTime(int parameterIndex, Time x, Calendar cal)
                throws SQLException
        {
            pstmt.setTime(parameterIndex, x, cal);
        }

        public void setTimestamp(int parameterIndex, Timestamp x) throws SQLException {
            pstmt.setTimestamp(parameterIndex, x);
        }

        public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal)
                throws SQLException
        {
            pstmt.setTimestamp(parameterIndex, x, cal);
        }

        public void setUnicodeStream(int parameterIndex, InputStream x, int length)
                throws SQLException
        {
            throw new UnsupportedOperationException();
        }

        // The following methods are from the Statement class - the
        // SuperInterface of PreparedStatement
        // without these this class won't compile

        public void addBatch(String sql) throws SQLException {
            pstmt.addBatch(sql);
        }

        public void cancel() throws SQLException {
            pstmt.cancel();
        }

        public void clearBatch() throws SQLException {
            pstmt.clearBatch();
        }

        public void clearWarnings() throws SQLException {
            pstmt.clearWarnings();
        }

        public void close() throws SQLException {
            pstmt.close();
        }

        public boolean execute(String _sql) throws SQLException {

            long t1 = System.currentTimeMillis();
            boolean result = pstmt.execute(_sql);
            long t2 = System.currentTimeMillis();

            // determine the type of query
            String sqlL = _sql.toLowerCase().trim();

            if (sqlL.startsWith("insert")) {
                addQuery(INSERT, _sql, t2-t1);
            }
            else if (sqlL.startsWith("update")) {
                addQuery(UPDATE, _sql, t2-t1);
            }
            else if (sqlL.startsWith("delete")) {
                addQuery(DELETE, _sql, t2-t1);
            }
            else {
                addQuery(SELECT, _sql, t2-t1);
            }
            return result;
        }

        public int[] executeBatch() throws SQLException {

            long t1 = System.currentTimeMillis();
            int[] result = pstmt.executeBatch();
            long t2 = System.currentTimeMillis();

            switch (type) {
                case SELECT:
                    addQuery(SELECT, sql, t2-t1);
                    break;
                case UPDATE:
                    addQuery(UPDATE, sql, t2-t1);
                    break;
                case INSERT:
                    addQuery(INSERT, sql, t2-t1);
                    break;
                case DELETE:
                    addQuery(DELETE, sql, t2-t1);
                    break;
            }
            return result;
        }

        public ResultSet executeQuery(String _sql) throws SQLException {
            long t1 = System.currentTimeMillis();
            ResultSet result = pstmt.executeQuery(_sql);
            long t2 = System.currentTimeMillis();

            // determine the type of query
            String sqlL = _sql.toLowerCase().trim();

            if (sqlL.startsWith("insert")) {
                addQuery(INSERT, _sql, t2-t1);
            }
            else if (sqlL.startsWith("update")) {
                addQuery(UPDATE, _sql, t2-t1);
            }
            else if (sqlL.startsWith("delete")) {
                addQuery(DELETE, _sql, t2-t1);
            }
            else {
                addQuery(SELECT, _sql, t2-t1);
            }
            return result;
        }

        public int executeUpdate(String _sql) throws SQLException {

            long t1 = System.currentTimeMillis();
            int result = pstmt.executeUpdate(_sql);
            long t2 = System.currentTimeMillis();

            // determine the type of query
            String sqlL = _sql.toLowerCase().trim();

            if (sqlL.startsWith("insert")) {
                addQuery(INSERT, _sql, t2-t1);
            }
            else if (sqlL.startsWith("update")) {
                addQuery(UPDATE, _sql, t2-t1);
            }
            else if (sqlL.startsWith("delete")) {
                addQuery(DELETE, _sql, t2-t1);
            }
            else {
                addQuery(SELECT, _sql, t2-t1);
            }
            return result;
        }

        public Connection getConnection() throws SQLException {
            return pstmt.getConnection();
        }

        public int getFetchDirection() throws SQLException {
            return pstmt.getFetchDirection();
        }

        public int getFetchSize()  throws SQLException {
            return pstmt.getFetchSize();
        }

        public int getMaxFieldSize() throws SQLException {
            return pstmt.getMaxFieldSize();
        }

        public int getMaxRows() throws SQLException {
            return pstmt.getMaxRows();
        }

        public boolean getMoreResults() throws SQLException {
            return pstmt.getMoreResults();
        }

        public int getQueryTimeout() throws SQLException {
            return pstmt.getQueryTimeout();
        }

        public ResultSet getResultSet() throws SQLException {
            return pstmt.getResultSet();
        }

        public int getResultSetConcurrency() throws SQLException {
            return pstmt.getResultSetConcurrency();
        }

        public int getResultSetType() throws SQLException {
            return pstmt.getResultSetType();
        }

        public int getUpdateCount() throws SQLException {
            return pstmt.getUpdateCount();
        }

        public SQLWarning getWarnings() throws SQLException {
            return pstmt.getWarnings();
        }

        public void setCursorName(String name) throws SQLException {
            pstmt.setCursorName(name);
        }

        public void setEscapeProcessing(boolean enable) throws SQLException {
            pstmt.setEscapeProcessing(enable);
        }

        public void setFetchDirection(int direction) throws SQLException {
            pstmt.setFetchDirection(direction);
        }

        public void setFetchSize(int rows) throws SQLException {
            pstmt.setFetchSize(rows);
        }

        public void setMaxFieldSize(int max) throws SQLException {
            pstmt.setMaxFieldSize(max);
        }

        public void setMaxRows(int max) throws SQLException {
            pstmt.setMaxRows(max);
        }

        public void setQueryTimeout(int seconds) throws SQLException {
            pstmt.setQueryTimeout(seconds);
        }

		public ParameterMetaData getParameterMetaData() throws SQLException {
			throw new UnsupportedOperationException(); 
		}

		public void setURL(int parameterIndex, URL x) throws SQLException {
			throw new UnsupportedOperationException(); 
		}

		public boolean execute(String sql, int autoGeneratedKeys)
				throws SQLException {
			throw new UnsupportedOperationException(); 
		}

		public boolean execute(String sql, int[] columnIndexes)
				throws SQLException {
			throw new UnsupportedOperationException(); 
		}

		public boolean execute(String sql, String[] columnNames)
				throws SQLException {
			throw new UnsupportedOperationException(); 
		}

		public int executeUpdate(String sql, int autoGeneratedKeys)
				throws SQLException {
			throw new UnsupportedOperationException(); 
		}

		public int executeUpdate(String sql, int[] columnIndexes)
				throws SQLException {
			throw new UnsupportedOperationException(); 
		}

		public int executeUpdate(String sql, String[] columnNames)
				throws SQLException {
			throw new UnsupportedOperationException(); 
		}

		public ResultSet getGeneratedKeys() throws SQLException {
			throw new UnsupportedOperationException(); 
		}

		public boolean getMoreResults(int current) throws SQLException {
			throw new UnsupportedOperationException(); 
		}

		public int getResultSetHoldability() throws SQLException {
			throw new UnsupportedOperationException(); 
		}

		public void setAsciiStream(int arg0, InputStream arg1)
				throws SQLException {
			throw new UnsupportedOperationException(); 			
		}

		public void setAsciiStream(int arg0, InputStream arg1, long arg2)
				throws SQLException {
			throw new UnsupportedOperationException(); 			
		}

		public void setBinaryStream(int arg0, InputStream arg1)
				throws SQLException {
			throw new UnsupportedOperationException(); 			
		}

		public void setBinaryStream(int arg0, InputStream arg1, long arg2)
				throws SQLException {
			throw new UnsupportedOperationException(); 			
		}

		public void setBlob(int arg0, InputStream arg1) throws SQLException {
			throw new UnsupportedOperationException(); 			
		}

		public void setBlob(int arg0, InputStream arg1, long arg2)
				throws SQLException {
			throw new UnsupportedOperationException(); 			
		}

		public void setCharacterStream(int arg0, Reader arg1)
				throws SQLException {
			throw new UnsupportedOperationException(); 			
		}

		public void setCharacterStream(int arg0, Reader arg1, long arg2)
				throws SQLException {
			throw new UnsupportedOperationException(); 			
		}

		public void setClob(int arg0, Reader arg1) throws SQLException {
			throw new UnsupportedOperationException(); 			
		}

		public void setClob(int arg0, Reader arg1, long arg2)
				throws SQLException {
			throw new UnsupportedOperationException(); 			
		}

		public void setNCharacterStream(int arg0, Reader arg1)
				throws SQLException {
			throw new UnsupportedOperationException(); 			
		}

		public void setNCharacterStream(int arg0, Reader arg1, long arg2)
				throws SQLException {
			throw new UnsupportedOperationException(); 			
		}

		public void setNClob(int arg0, Reader arg1) throws SQLException {
			throw new UnsupportedOperationException(); 			
		}

		public void setNClob(int arg0, Reader arg1, long arg2)
				throws SQLException {
			throw new UnsupportedOperationException(); 			
		}

		public void setNString(int arg0, String arg1) throws SQLException {
			throw new UnsupportedOperationException(); 			
		}

		public boolean isClosed() throws SQLException {
			throw new UnsupportedOperationException(); 
		}

		public boolean isPoolable() throws SQLException {
			throw new UnsupportedOperationException(); 
		}

		public void setPoolable(boolean arg0) throws SQLException {
			throw new UnsupportedOperationException(); 
		}

		public boolean isWrapperFor(Class<?> iface) throws SQLException {
			throw new UnsupportedOperationException(); 
		}

		public <T> T unwrap(Class<T> iface) throws SQLException {
			throw new UnsupportedOperationException(); 
		}
    }

	public Statement createStatement(int resultSetType,
			int resultSetConcurrency, int resultSetHoldability)
			throws SQLException {
		throw new UnsupportedOperationException(); 
	}

	public int getHoldability() throws SQLException {
		throw new UnsupportedOperationException(); 
	}

	public CallableStatement prepareCall(String sql, int resultSetType,
			int resultSetConcurrency, int resultSetHoldability)
			throws SQLException {
		throw new UnsupportedOperationException(); 
	}

	public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys)
			throws SQLException {
		throw new UnsupportedOperationException(); 
	}

	public PreparedStatement prepareStatement(String sql, int[] columnIndexes)
			throws SQLException {
		throw new UnsupportedOperationException(); 
	}

	public PreparedStatement prepareStatement(String sql, String[] columnNames)
			throws SQLException {
		throw new UnsupportedOperationException(); 
	}

	public PreparedStatement prepareStatement(String sql, int resultSetType,
			int resultSetConcurrency, int resultSetHoldability)
			throws SQLException {
		throw new UnsupportedOperationException(); 
	}

	public void releaseSavepoint(Savepoint savepoint) throws SQLException {
		throw new UnsupportedOperationException(); 
	}

	public void rollback(Savepoint savepoint) throws SQLException {
		throw new UnsupportedOperationException(); 
	}

	public void setHoldability(int holdability) throws SQLException {
		throw new UnsupportedOperationException(); 
	}

	public Savepoint setSavepoint() throws SQLException {
		throw new UnsupportedOperationException(); 
	}

	public Savepoint setSavepoint(String name) throws SQLException {
		throw new UnsupportedOperationException(); 
	}

	public Array createArrayOf(String typeName, Object[] elements)
			throws SQLException {
		throw new UnsupportedOperationException(); 
	}

	public Blob createBlob() throws SQLException {
		throw new UnsupportedOperationException(); 
	}

	public Clob createClob() throws SQLException {
		throw new UnsupportedOperationException(); 
	}

	public Struct createStruct(String typeName, Object[] attributes)
			throws SQLException {
		throw new UnsupportedOperationException(); 
	}

	public Properties getClientInfo() throws SQLException {
		throw new UnsupportedOperationException(); 
	}

	public String getClientInfo(String name) throws SQLException {
		throw new UnsupportedOperationException(); 
	}

	public boolean isValid(int timeout) throws SQLException {
		throw new UnsupportedOperationException(); 
	}

	public boolean isWrapperFor(Class<?> iface) throws SQLException {
		throw new UnsupportedOperationException(); 
	}

	public <T> T unwrap(Class<T> iface) throws SQLException {
		throw new UnsupportedOperationException(); 
	}
}